/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.core;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.CometEvent;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.ErrorPage;
import org.apache.catalina.util.RequestUtil;
import org.apache.catalina.util.StringManager;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Valve that implements the default basic behavior for the
 * <code>StandardHost</code> container implementation.
 * <p>
 * <b>USAGE CONSTRAINT</b>: This implementation is likely to be useful only
 * when processing HTTP requests.
 * 
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 * @version $Revision: 487694 $ $Date: 2006-12-16 06:30:51 +0800 (星期六, 16 十二月
 *          2006) $
 */

final class StandardHostValve extends ValveBase {

	private static Log log = LogFactory.getLog(StandardHostValve.class);

	// ----------------------------------------------------- Instance Variables

	/**
	 * The descriptive information related to this implementation.
	 */
	private static final String info = "org.apache.catalina.core.StandardHostValve/1.0";

	/**
	 * The string manager for this package.
	 */
	private static final StringManager sm = StringManager
			.getManager(Constants.Package);

	// ------------------------------------------------------------- Properties

	/**
	 * Return descriptive information about this Valve implementation.
	 */
	public String getInfo() {

		return (info);

	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Select the appropriate child Context to process this request, based on
	 * the specified request URI. If no matching Context can be found, return an
	 * appropriate HTTP error.
	 * 
	 * @param request
	 *            Request to be processed
	 * @param response
	 *            Response to be produced
	 * @param valveContext
	 *            Valve context used to forward to the next Valve
	 * 
	 * @exception IOException
	 *                if an input/output error occurred
	 * @exception ServletException
	 *                if a servlet error occurred
	 */
	public final void invoke(Request request, Response response)
			throws IOException, ServletException {

		// Select the Context to be used for this Request
		Context context = request.getContext();
		if (context == null) {
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, sm
					.getString("standardHost.noContext"));
			return;
		}

		// Bind the context CL to the current thread
		if (context.getLoader() != null) {
			// Not started - it should check for availability first
			// This should eventually move to Engine, it's generic.
			Thread.currentThread().setContextClassLoader(
					context.getLoader().getClassLoader());
		}

		// Ask this Context to process this request
		context.getPipeline().getFirst().invoke(request, response);

		// Access a session (if present) to update last accessed time, based on
		// a
		// strict interpretation of the specification
		if (Globals.STRICT_SERVLET_COMPLIANCE) {
			request.getSession(false);
		}

		// Error page processing
		response.setSuspended(false);

		Throwable t = (Throwable) request.getAttribute(Globals.EXCEPTION_ATTR);

		if (t != null) {
			throwable(request, response, t);
		} else {
			status(request, response);
		}

		// Restore the context classloader
		Thread.currentThread().setContextClassLoader(
				StandardHostValve.class.getClassLoader());

	}

	/**
	 * Process Comet event.
	 * 
	 * @param request
	 *            Request to be processed
	 * @param response
	 *            Response to be produced
	 * @param valveContext
	 *            Valve context used to forward to the next Valve
	 * 
	 * @exception IOException
	 *                if an input/output error occurred
	 * @exception ServletException
	 *                if a servlet error occurred
	 */
	public final void event(Request request, Response response, CometEvent event)
			throws IOException, ServletException {

		// Select the Context to be used for this Request
		Context context = request.getContext();

		// Bind the context CL to the current thread
		if (context.getLoader() != null) {
			// Not started - it should check for availability first
			// This should eventually move to Engine, it's generic.
			Thread.currentThread().setContextClassLoader(
					context.getLoader().getClassLoader());
		}

		// Ask this Context to process this request
		context.getPipeline().getFirst().event(request, response, event);

		// Access a session (if present) to update last accessed time, based on
		// a
		// strict interpretation of the specification
		if (Globals.STRICT_SERVLET_COMPLIANCE) {
			request.getSession(false);
		}

		// Error page processing
		response.setSuspended(false);

		Throwable t = (Throwable) request.getAttribute(Globals.EXCEPTION_ATTR);

		if (t != null) {
			throwable(request, response, t);
		} else {
			status(request, response);
		}

		// Restore the context classloader
		Thread.currentThread().setContextClassLoader(
				StandardHostValve.class.getClassLoader());

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Handle the specified Throwable encountered while processing the specified
	 * Request to produce the specified Response. Any exceptions that occur
	 * during generation of the exception report are logged and swallowed.
	 * 
	 * @param request
	 *            The request being processed
	 * @param response
	 *            The response being generated
	 * @param throwable
	 *            The exception that occurred (which possibly wraps a root cause
	 *            exception
	 */
	protected void throwable(Request request, Response response,
			Throwable throwable) {
		Context context = request.getContext();
		if (context == null)
			return;

		Throwable realError = throwable;

		if (realError instanceof ServletException) {
			realError = ((ServletException) realError).getRootCause();
			if (realError == null) {
				realError = throwable;
			}
		}

		// If this is an aborted request from a client just log it and return
		if (realError instanceof ClientAbortException) {
			if (log.isDebugEnabled()) {
				log.debug(sm.getString("standardHost.clientAbort", realError
						.getCause().getMessage()));
			}
			return;
		}

		ErrorPage errorPage = findErrorPage(context, throwable);
		if ((errorPage == null) && (realError != throwable)) {
			errorPage = findErrorPage(context, realError);
		}

		if (errorPage != null) {
			response.setAppCommitted(false);
			request.setAttribute(
					ApplicationFilterFactory.DISPATCHER_REQUEST_PATH_ATTR,
					errorPage.getLocation());
			request.setAttribute(ApplicationFilterFactory.DISPATCHER_TYPE_ATTR,
					new Integer(ApplicationFilterFactory.ERROR));
			request.setAttribute(Globals.STATUS_CODE_ATTR, new Integer(
					HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
			request.setAttribute(Globals.ERROR_MESSAGE_ATTR, throwable
					.getMessage());
			request.setAttribute(Globals.EXCEPTION_ATTR, realError);
			Wrapper wrapper = request.getWrapper();
			if (wrapper != null)
				request.setAttribute(Globals.SERVLET_NAME_ATTR, wrapper
						.getName());
			request.setAttribute(Globals.EXCEPTION_PAGE_ATTR, request
					.getRequestURI());
			request.setAttribute(Globals.EXCEPTION_TYPE_ATTR, realError
					.getClass());
			if (custom(request, response, errorPage)) {
				try {
					response.flushBuffer();
				} catch (IOException e) {
					container.getLogger().warn(
							"Exception Processing " + errorPage, e);
				}
			}
		} else {
			// A custom error-page has not been defined for the exception
			// that was thrown during request processing. Check if an
			// error-page for error code 500 was specified and if so,
			// send that page back as the response.
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			// The response is an error
			response.setError();

			status(request, response);
		}

	}

	/**
	 * Handle the HTTP status code (and corresponding message) generated while
	 * processing the specified Request to produce the specified Response. Any
	 * exceptions that occur during generation of the error report are logged
	 * and swallowed.
	 * 
	 * @param request
	 *            The request being processed
	 * @param response
	 *            The response being generated
	 */
	protected void status(Request request, Response response) {

		int statusCode = response.getStatus();

		// Handle a custom error page for this status code
		Context context = request.getContext();
		if (context == null)
			return;

		/*
		 * Only look for error pages when isError() is set. isError() is set
		 * when response.sendError() is invoked. This allows custom error pages
		 * without relying on default from web.xml.
		 */
		if (!response.isError())
			return;

		ErrorPage errorPage = context.findErrorPage(statusCode);
		if (errorPage != null) {
			response.setAppCommitted(false);
			request.setAttribute(Globals.STATUS_CODE_ATTR, new Integer(
					statusCode));

			String message = RequestUtil.filter(response.getMessage());
			if (message == null)
				message = "";
			request.setAttribute(Globals.ERROR_MESSAGE_ATTR, message);
			request.setAttribute(
					ApplicationFilterFactory.DISPATCHER_REQUEST_PATH_ATTR,
					errorPage.getLocation());
			request.setAttribute(ApplicationFilterFactory.DISPATCHER_TYPE_ATTR,
					new Integer(ApplicationFilterFactory.ERROR));

			Wrapper wrapper = request.getWrapper();
			if (wrapper != null)
				request.setAttribute(Globals.SERVLET_NAME_ATTR, wrapper
						.getName());
			request.setAttribute(Globals.EXCEPTION_PAGE_ATTR, request
					.getRequestURI());
			if (custom(request, response, errorPage)) {
				try {
					response.flushBuffer();
				} catch (ClientAbortException e) {
					// Ignore
				} catch (IOException e) {
					container.getLogger().warn(
							"Exception Processing " + errorPage, e);
				}
			}
		}

	}

	/**
	 * Find and return the ErrorPage instance for the specified exception's
	 * class, or an ErrorPage instance for the closest superclass for which
	 * there is such a definition. If no associated ErrorPage instance is found,
	 * return <code>null</code>.
	 * 
	 * @param context
	 *            The Context in which to search
	 * @param exception
	 *            The exception for which to find an ErrorPage
	 */
	protected static ErrorPage findErrorPage(Context context,
			Throwable exception) {

		if (exception == null)
			return (null);
		Class clazz = exception.getClass();
		String name = clazz.getName();
		while (!Object.class.equals(clazz)) {
			ErrorPage errorPage = context.findErrorPage(name);
			if (errorPage != null)
				return (errorPage);
			clazz = clazz.getSuperclass();
			if (clazz == null)
				break;
			name = clazz.getName();
		}
		return (null);

	}

	/**
	 * Handle an HTTP status code or Java exception by forwarding control to the
	 * location included in the specified errorPage object. It is assumed that
	 * the caller has already recorded any request attributes that are to be
	 * forwarded to this page. Return <code>true</code> if we successfully
	 * utilized the specified error page location, or <code>false</code> if
	 * the default error report should be rendered.
	 * 
	 * @param request
	 *            The request being processed
	 * @param response
	 *            The response being generated
	 * @param errorPage
	 *            The errorPage directive we are obeying
	 */
	protected boolean custom(Request request, Response response,
			ErrorPage errorPage) {

		if (container.getLogger().isDebugEnabled())
			container.getLogger().debug("Processing " + errorPage);

		request.setPathInfo(errorPage.getLocation());

		try {

			// Reset the response if possible (else IllegalStateException)
			// hres.reset();
			// Reset the response (keeping the real error code and message)
			Integer statusCodeObj = (Integer) request
					.getAttribute(Globals.STATUS_CODE_ATTR);
			int statusCode = statusCodeObj.intValue();
			String message = (String) request
					.getAttribute(Globals.ERROR_MESSAGE_ATTR);
			response.reset(statusCode, message);

			// Forward control to the specified location
			ServletContext servletContext = request.getContext()
					.getServletContext();
			RequestDispatcher rd = servletContext
					.getRequestDispatcher(errorPage.getLocation());
			rd.forward(request.getRequest(), response.getResponse());

			// If we forward, the response is suspended again
			response.setSuspended(false);

			// Indicate that we have successfully processed this custom page
			return (true);

		} catch (Throwable t) {

			// Report our failure to process this custom page
			container.getLogger().error("Exception Processing " + errorPage, t);
			return (false);

		}

	}

}
